#include "extras/transformObject.h"
#include "vboGrid.h"

#include "application.h"


#include <GL/glew.h>
#include <GL/glut.h>

#include <cutil_inline.h>
#include <nvVector.h>


#include "extras/utils.h"
#include "extras/cfgReader.h"
#include "extras/texture.h"
#include "extras/skybox.h"
#include "extras/textureMaterial.h"
#include "extras/shaderMaterial.h"
#include "image.h"

Application *Application::app = 0;

Application::Application() {

}

Application::~Application() {

	glutLeaveGameMode();

	delete camera;

	delete camera_speed;
	delete terrain;
	delete skyBox;

	delete recordingTex;
	Texture::clearAllTextures();

}

Application * Application::getInstance() {

	if (!app)
		app = new Application();

	return app;

}

void Application::destroy(){

	if(app)
		delete app;

	app = 0;
}

void Application::init(int argc, char **argv) {

	// set up camera
	mouse_btnState = 0;
	camera = new TransformObject();
	camera_speed = new nv::vec3f(0, 0, 0);
	camera_speed_mod = CfgReader::strToDouble(CfgReader::getValue(
			"camMoveSpeed"));
	camera_rotation_mod = CfgReader::strToDouble(CfgReader::getValue(
			"camRotationSpeed"));
	camera->Set(nv::vec3f(0, 1, -1), nv::vec3f(0, 1, 0), nv::vec3f(0, 1, 0));

	view_width = 640;
	view_height = 480;

	// set up fps stuff
	timebase = glutGet(GLUT_ELAPSED_TIME);
	frame = 0;
	totalFrame = 0;
	fps = 0.0001f;

	// show debug data
	showDebugText = (bool) CfgReader::strToInt(CfgReader::getValue(
			"showDebugText"));
	fullScreen = (bool) CfgReader::strToInt(CfgReader::getValue("fullScreen"));
	recording = (bool) CfgReader::strToInt(CfgReader::getValue("recordToFile"));
	recordFps = 1.f / (CfgReader::strToDouble(CfgReader::getValue("recordFps")));
	recordSteps = CfgReader::strToInt(CfgReader::getValue("recordSteps"));
	recordLocation = CfgReader::getValue("recordLocation");
	recordedFrames = 0;

	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH);

	setFullScreen();

	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);

	float ambientLight[] = { 0.2f, 0.2f, 0.2f, 1.0f };
	float diffuseLight[] = { 0.5f, 0.5f, 0.5f, 1.0f };
	float specularLight[] = { 0.8f, 0.8f, 0.8f, 1.0f };

	glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);
	glLightfv(GL_LIGHT0, GL_SPECULAR, specularLight);


	glEnable(GL_TEXTURE_2D); // Enable Texture Mapping ( NEW )
	glShadeModel(GL_SMOOTH); // Enable Smooth Shading
	glClearDepth(1.0f); // Depth Buffer Setup
	glEnable(GL_DEPTH_TEST); // Enables Depth Testing
	glDepthMask(GL_TRUE);
	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);
	glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Really Nice Perspective Calculations
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);




	glewInit();

	glPrimitiveRestartIndexNV(0xffffffff); // set restart index for indexpointing vbo
	glEnableClientState(GL_PRIMITIVE_RESTART_NV);

	// Set up projection matrix

	reshape(view_width, view_height);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	skyBox = new SkyBox("","", 40);

	recordingTex = new TextureMaterial();
	recordingTex->addTexture(Texture::loadFileTexture("textures/recording.tga"), "");

	terrain = new VboGrid(75, 320, 2.3f);

	Image *simplexPerm = Image::createPermutationMap();

	TextureMaterial *terrainTextures = new TextureMaterial();
	terrainTextures->addTexture(Texture::loadImageTexture(simplexPerm), "permTexture");
	terrainTextures->addTexture(Texture::loadFileTexture("textures/dirt.JPG"), "region1");
	terrainTextures->addTexture(Texture::loadFileTexture("textures/grass.JPG"), "region2");
	terrainTextures->addTexture(Texture::loadFileTexture("textures/rock.JPG"), "region3");
	terrainTextures->addTexture(Texture::loadFileTexture("textures/snow.JPG"), "region4");

	terrainShader = new ShaderMaterial(nv::vec4f(1.,1.,1.,1.), nv::vec4f(0.,1.,1.,1.), 0.0f);
	terrainShader->loadShaders("shaders/terrain.vert","shaders/terrain.frag");
	terrainShader->setTextureMaterial(terrainTextures);

	delete simplexPerm;

	glutMainLoop();

}

void Application::render() {

	GLfloat bgColor[4] = {0.6, 0.6, 0.7, 1.0};


	glClearColor(bgColor[0],bgColor[1],bgColor[2],bgColor[3]);
	glClear(GL_COLOR_BUFFER_BIT + GL_DEPTH_BUFFER_BIT);

	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadIdentity();

	camera->move((*camera_speed) * camera_speed_mod * Application::getFps());

	nv::vec3f pos = camera->m_position;
	camAngle = camera->getHeading();

	//float framerate = (recording) ? recordFps : ((fps < 0.2f) ? fps : 0.2f );

	camera->multToGL();

	// make sure skybox moves with camera
	glPushMatrix();

		glTranslatef(pos.x, pos.y, pos.z);
		//skyBox->draw();

	glPopMatrix();

	// set light position after camera transform to make sure it stays constant
	float position[] = { -39.f, 34.f, 20.f, 0.0f };
	glLightfv(GL_LIGHT0, GL_POSITION, position);
	glEnable(GL_LIGHTING);


	GLfloat density = 0.2;

	glFogi (GL_FOG_MODE, GL_EXP2);
	glFogfv (GL_FOG_COLOR, bgColor);
	glFogf (GL_FOG_DENSITY, density);
	glHint (GL_FOG_HINT, GL_NICEST);
	glFogf(GL_FOG_START, 3.5f);				// Fog Start Depth
	//glFogf(GL_FOG_END, 4.0f);				// Fog End Depth
	glEnable (GL_FOG);
/*
	GLUquadric *q = gluNewQuadric();
	gluQuadricDrawStyle(q, GLU_FILL);
	gluSphere(q, .2, 20, 20);
	gluDeleteQuadric(q);
*/
	glPushMatrix();

		glTranslatef(pos.x, 0.f, pos.z);


		glBegin(GL_QUADS);
		glTexCoord2f(0.0f, 0.0f);
		glVertex3f(-100.0f, 0.2f, -100.0f);
		glTexCoord2f(1.0f, 0.0f);
		glVertex3f(-100.0f, 0.2f, 100.0f);
		glTexCoord2f(1.0f, 1.0f);
		glVertex3f(100.0f, 0.2f, 100.0f);
		glTexCoord2f(0.0f, 1.0f);
		glVertex3f(100.0f, 0.2f, -100.0f);
		glEnd();

	glPopMatrix();

	glPushMatrix();

		//glTranslatef(pos.x, 0.f, pos.z);
		//glRotatef(camAngle*180./M_PI, 0.,1.f,0.);


		// draw terrain **************************************
		terrainShader->begin();

		int loc = terrainShader->getUniform("cameraPosition");
		glUniform4f(loc, pos.x, 0, pos.z, 0.);

		float min = 0.f;float max = 0.5f;
		loc = terrainShader->getUniform("regionD1");
		glUniform4f(loc, min, max, max - min, 1.f/(max - min));

		min = 0.5f;max = 1.0f;
		loc = terrainShader->getUniform("regionD2");
		glUniform4f(loc, min, max, max - min, 1.f/(max - min));

		min = 1.f;max = 1.5f;
		loc = terrainShader->getUniform("regionD3");
		glUniform4f(loc, min, max, max - min, 1.f/(max - min));

		min = 1.5f;max = 2.5f;
		loc = terrainShader->getUniform("regionD4");
		glUniform4f(loc, min, max, max - min, 1.f/(max - min));

		loc = terrainShader->getUniform("cameraAngle");
		glUniform1f(loc, camAngle);

		terrain->draw();
		terrainShader->end();


	glPopMatrix();

	glPopMatrix();
	glDisable (GL_FOG);
	glDisable(GL_LIGHTING);

	// when recording save screenshots to file
	if(recording){
		char s[256];
		sprintf(s, "%s/frame_%d.jpg", recordLocation.c_str(), totalFrame);
		Texture::saveBuffer(view_width, view_height, s);

		++recordedFrames;
	}

	if (showDebugText || recording)
		renderDebugText();


	glutSwapBuffers();

	// calculate framerates
	++frame;
	++totalFrame;
	int time = glutGet(GLUT_ELAPSED_TIME);
	if (time - timebase > 100) {

		fps = (time - timebase)/(frame * 1000.0f);

		timebase = time;
		frame = 0;
	}
}
;

void Application::renderDebugText() {

	glDisable(GL_LIGHTING);

	// no textures
	glBindTexture(GL_TEXTURE_2D, 0);

	glColor3f(1.0, 1.0, 1.0);

	// switch to projection mode
	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();

	// set a 2D orthographic projection
	gluOrtho2D(0, view_width, 0, view_height);
	// invert the y axis, down is positive
	glScalef(1, -1, 1);
	// mover the origin from the bottom left corner
	// to the upper left corner
	glTranslatef(0, -view_height, 0);
	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();

	glLoadIdentity();

	int x = 5;
	int y = 17;

	char s[256];
	sprintf(s, "%ix%ipx\nFPS: %4.2f\nTextures: %4.2fMB\nPitch: %4.2fdeg\nHeading: %4.2fdeg", view_width,
			view_height, 1.f/Application::getFps(),
			Texture::getMemoryUsage() / 1048576.f,
			camera->getPitch()*180./M_PI,
			camera->getHeading()*180./M_PI);

	Utils::printText(s, x, y, 14, 2);

	if (recording) {

		x = view_width - 124;
		y = view_height - 32;

		char r[128];
		sprintf(r, "Recording\n(frames: %i)", recordedFrames);

		Utils::printText(r, x, y, 14, 2);

		glPushMatrix();
		glTranslatef(view_width - 139.f, view_height - 41.f, 0.0f);

		recordingTex->begin();

		glBegin(GL_QUADS);
		glTexCoord2f(0.0f, 0.0f);
		glVertex3f(0.0f, 0.0f, 0.0f);
		glTexCoord2f(1.0f, 0.0f);
		glVertex3f(0.0f, 10.0f, 0.0f);
		glTexCoord2f(1.0f, 1.0f);
		glVertex3f(10.0f, 10.0f, 0.0f);
		glTexCoord2f(0.0f, 1.0f);
		glVertex3f(10.0f, 0.0f, 0.0f);
		glEnd();

		recordingTex->end();

		glPopMatrix();
	}

	glPopMatrix();
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);

	glEnable(GL_LIGHTING);

}

void Application::setFullScreen() {

	if (!fullScreen)
		glutLeaveGameMode();

	int currentWin = glutGetWindow();
	glutDestroyWindow(currentWin);

	if (fullScreen) {
		glutGameModeString(CfgReader::getValue("fullScreenMode").c_str());

		// enter full screen
		if (glutGameModeGet(GLUT_GAME_MODE_POSSIBLE))
			glutEnterGameMode();
		else {
			printf("The selected fullscreen mode is not available\n");
			exit(1);
		}
		glutSetCursor(GLUT_CURSOR_NONE);
		view_width = glutGet(GLUT_WINDOW_WIDTH);
		view_height = glutGet(GLUT_WINDOW_HEIGHT);
		glutWarpPointer(view_width * .5, view_height * .5);
		mouse_x = view_width * .5;
		mouse_y = view_height * .5;

	} else {

		glutInitWindowSize(640, 480);
		glutInitWindowPosition(100, 100);
		glutCreateWindow("Cudaname");
		glutSetCursor(GLUT_CURSOR_LEFT_ARROW);

	}

	glutDisplayFunc(g_render);
	glutIdleFunc(g_idle);
	glutSpecialFunc(g_keyboardSpecial);
	glutSpecialUpFunc(g_keyboardSpecialUp);
	glutKeyboardFunc(g_keyboard);
	glutKeyboardUpFunc(g_keyboardUp);
	glutMouseFunc(g_mouseAction);
	glutMotionFunc(g_mouseMove);
	glutPassiveMotionFunc(g_mouseMove);
	glutReshapeFunc(g_reshape);

}

void Application::keyDown(int key, int, int) {


	switch (key) {

	case 3:
		showDebugText = !showDebugText;
		break;
	case 27:
		glutLeaveGameMode();
		exit(1);
		break;
		//case 100:
	case 'a':
		camera_speed->x += 1;
		break;
		//case 101:
	case 'w':
		camera_speed->z += 1;
		break;
		//case 102:
	case 'd':
		camera_speed->x += -1;
		break;
		//case 103:
	case 's':
		camera_speed->z += -1;
		break;
	case 'p':
	case 32:
		// pause
		break;
	case 'r':
		recording = !recording;
	case 'f':
		char s[256];
		sprintf(s, "%s/screenshot_%d.jpg", ".", totalFrame);
		Texture::saveBuffer(view_width, view_height, s);

		break;
	}


	glutPostRedisplay();
}

void Application::keyUp(int key, int, int) {

	switch (key) {

	//case 100:
	case 'a':
		camera_speed->x -= 1;
		break;
		//case 101:
	case 'w':
		camera_speed->z -= 1;
		break;
		//case 102:
	case 'd':
		camera_speed->x -= -1;
		break;
		//case 103:
	case 's':
		camera_speed->z -= -1;
		break;

	}

	glutPostRedisplay();
}

void Application::mouseAction(int button, int state, int x, int y) {
	if (state == GLUT_DOWN)
		mouse_btnState |= 1 << button;
	else if (state == GLUT_UP)
		mouse_btnState = 0;

	mouse_x = x;
	mouse_y = y;

	glutPostRedisplay();
}

void Application::mouseMove(int x, int y) {
	if (x == mouse_x && y == mouse_y)
		return;

	float dx, dy;
	dx = x - mouse_x;
	dy = y - mouse_y;

	if (mouse_btnState & 1 || fullScreen == true) {
		// left = translate
		camera->rotate(dx * -camera_rotation_mod, dy * -camera_rotation_mod);
	} else if (mouse_btnState & 2) {
		// middle = zoom

	}

	if (fullScreen) {
		mouse_x = view_width * .5;
		mouse_y = view_height * .5;
		glutWarpPointer(mouse_x, mouse_y);
	} else {
		mouse_x = x;
		mouse_y = y;
	}
	//glutPostRedisplay();
}

void Application::idle() {
	glutPostRedisplay();
}

void Application::reshape(int w, int h) {

	if (h == 0)
		h = 1;

	float ratio = 1.0 * w / h;

	view_width = w;
	view_height = h;

	// Reset the coordinate system before modifying
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	// Set the viewport to be the entire window
	glViewport(0, 0, w, h);

	// Set the correct perspective.
	gluPerspective(45, ratio, .1, 1000);
	glMatrixMode(GL_MODELVIEW);

}

